#!/bin/sh
#
# Copyright (c) 2007-2008 The PureDarwin Project.
# All rights reserved.
#
# @LICENSE_HEADER_START@
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# @LICENSE_HEADER_END@
#

#
# aladin <aladin@puredarwin.org>
#

#
# Add users without dscl in Darwin 9
# based on pd_adduser
#

#
# Changelog
#
# 20080321 - See `hg log' (mercurial) from now - aladin
# 20081211 - Adding shell choice - aladin
# 20081209 - License header update - aladin
# 20080831 - Minor updates - aladin
# 20080827 - Minor updates
#            typo fixed - aladin
# 20080817 - <user>.state file added
#            Minor updates - aladin
# 20080808 - Initial version - aladin
#

# Ensure root exclusivity
if [ "$UID" -ne 0 ]
then
	echo "You must be root in order to use $(basename $0)"
	exit 1
fi

#
# Set up variables
#

OPENSSL="/usr/bin/openssl"
GREP="/usr/bin/grep"
HEAD="/usr/bin/head"
SORT="/usr/bin/sort"
XXD="/usr/bin/xxd"
AWK="/usr/bin/awk"

# Mount devfs (e.g., /dev/null, /dev/random, etc.. are needed)
/sbin/mount_devfs devfs /dev

# Default target volume
MOUNT=/
if [ "$1" != "" ]
then
	# Ensure "target" exists
	if [ ! -e "$1" ]
	then
		echo "Unknown volume name: $1"
		exit -1
	fi	
	# Set new target
	MOUNT="$1"
fi
echo "Target volume name: $MOUNT"
echo ""

# User existence
EXISTS=0

# Usage
#
# ./pd_injectuser target default_username 
# $0              $1     $2       
T_USERNAME=$2

#
# Interactive
#

# Prompt for username (if no arg specified above)
if [ "$T_USERNAME" = "" ]; then
	# Infinite loop until username is not blank
	while [ 1 ]; do
		printf "Username: "
		read T_USERNAME;

		if [ ! "$T_USERNAME" = "" ]; then
			break
		else
			echo "Username must not be blank, try again."
		fi
	done
else
	T_DEFAULT="TRUE"
	echo "Entering default mode"
fi


# Check whether user already exists
if [ -e ${MOUNT}/private/var/db/dslocal/nodes/Default/users/${T_USERNAME}.plist ]; then
	echo "${T_USERNAME} already exists"

	if [ "$T_DEFAULT" = "" ]; then
		printf "Overwrite? y[n]: "
		read CHOICE
	else
		echo "Overwrite forced by default mode"
		CHOICE="y"
	fi

	if [ "$CHOICE" = "y" ]; then
		EXISTS=1
	else
		echo "Aborted."
		exit 1
	fi
fi

# Prompt for realname (if no arg specified above)
if [ "$T_DEFAULT" = "" ]; then
	printf "Realname: "
	read T_REALNAME
else
	T_REALNAME="$T_USERNAME"
fi

# Generating a random Universally Unique IDentifier (UUID)
#
# 	ie for user: e0d36085-8a80-238b-abfb-c537c7b36378
# 	ie for root: FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000
#
T_UUID="`    $OPENSSL rand 4 | $XXD -p -u`-`$OPENSSL rand 2 |
$XXD -p -u`-`$OPENSSL rand 2 | $XXD -p -u`-`$OPENSSL rand 2 |
$XXD -p -u`-`$OPENSSL rand 6 | $XXD -p -u`"

# Find max uid
M_UID=`cat ${MOUNT}/var/db/dslocal/nodes/Default/users/*.plist |
$GREP -v generateduid | $GREP -A2 uid  |
$GREP string | $AWK -F ">|<" ' {printf $3"\n"}' |
$SORT -rg | $HEAD -n1`

if [ "$T_USERNAME" == "root" ]; then
	# echo "Warning: Security consequences involved"
	M_UID=0
	T_UUID="FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000"
else
	if [ $M_UID -lt 1000 ]; then
		M_UID=1000
	else
		M_UID=`expr $M_UID + 1`
	fi
fi

T_UID_DEFAULT=$M_UID
T_GID_DEFAULT=$M_UID

if [ "$T_DEFAULT" = "" ]; then
	printf "UID (default: $T_UID_DEFAULT): "
	read T_UID

	printf "GID (default: $T_GID_DEFAULT): "
	read T_GID
fi

if [ "$T_UID" = "" ]; then
	T_UID=${T_UID_DEFAULT}
fi

#if [ "$T_UID" = "0" ]; then
#	echo "Warning: Security consequences involved"
#fi

#if [ "$T_GID" = "" ]; then
#	T_GID=${T_GID_DEFAULT}
#fi

if [ "$T_GID" = "0" ]; then
	echo "Warning: Security consequences involved"
fi

if [ "$T_USERNAME" == "root" ]; then
	T_HOMEDIR_DEFAULT="/var/$T_USERNAME"
else
	T_HOMEDIR_DEFAULT="/Users/$T_USERNAME"
fi


# Default shell
T_SHELL_DEFAULT=/bin/zsh
#SHELL_VALID=0

if [ "$T_DEFAULT" = "" ]; then
	# Prompt for default shell
	echo "Available shells:"
	echo "`cat $MOUNT/etc/shells | $GREP bin`"
	# Infinite loop until a valid shell is entered
	while [ 1 ]; do
		printf "Shell (default: $T_SHELL_DEFAULT): "
		read T_SHELL;
	
		if [ ! "$T_SHELL" = "" ]; then
			$GREP $T_SHELL $MOUNT/etc/shells  > /dev/null 2> /dev/null
			if [ $? -eq 0 ]; then
				break
			else
				echo "Insert valid shell, try again."
			fi
		else
			break
		fi
	done
fi

# Default shell
if [ "$T_SHELL" = "" ]; then
	T_SHELL=$T_SHELL_DEFAULT
fi

# Prompt for home directory
if [ "$T_DEFAULT" = "" ]; then
	printf "Home directory (default: $T_HOMEDIR_DEFAULT): "
	read T_HOMEDIR;
fi

# Default home directory
if [ "$T_HOMEDIR" = "" ]; then
	T_HOMEDIR=$T_HOMEDIR_DEFAULT
fi

# Check whether home directory already exists
if [ -e "${MOUNT}${T_HOMEDIR}" ]; then
	echo "${MOUNT}${T_HOMEDIR} already exists"
	#exit1
fi

# Prompt for admin group
if [ "$T_DEFAULT" = "" ]; then
	printf "Add ${T_USERNAME} to the admin group? [y]n: "
	read CHOICE;
	if [ ! "$CHOICE" = "n" ]; then
		ADMIN_USER=1
	fi
else
	echo "Granted (Admin) by default mode"
	ADMIN_USER=1
fi

# Disabling tty output
stty -echo

if [ "$T_DEFAULT" = "" ]; then
	# Infinite loop until password was entered the same twice
	while [ 1 ]; do
		printf "Password: "
		read T_PASSWORD;
		echo ""

		printf "Verifying - Password: "
		read V_PASSWORD;
		echo ""

		if [ "$T_PASSWORD" = "$V_PASSWORD" ]; then
			break
		else
			echo "Verify failure"
		fi
	done
else
	T_PASSWORD="$T_USERNAME"
fi

# Enabling tty output
stty echo

# Display summary
echo ""
echo "Generated UUID: ${T_UUID}"
echo "Username: ${T_USERNAME}"
echo "Realname: ${T_REALNAME}"
echo "Password: ********"
echo "UID: ${T_UID}"
if [ "$ADMIN_USER" == "1" ]; then
	echo "GID: 80, ${T_GID}"
else
	echo "GID: ${T_GID}"
fi
echo "Shell: ${T_SHELL}"
echo "Home: ${T_HOMEDIR}"
echo ""

if [ "$T_DEFAULT" = "" ]; then
	printf "Add [y]n: "
	read CHOICE
	if [ "$CHOICE" = "n" ]; then
		echo "Aborted."
		exit 0
	fi
fi
#
# Create user plist file
#

USER_PLIST="${MOUNT}/var/db/dslocal/nodes/Default/users/${T_USERNAME}.plist"

cat >$USER_PLIST<<EOOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>authentication_authority</key>
	<array>
		<string>;ShadowHash;</string>
	</array>
	<key>generateduid</key>
	<array>
		<string>${T_UUID}</string>
	</array>
	<key>gid</key>
	<array>
		<string>${T_GID}</string>
	</array>
	<key>home</key>
	<array>
		<string>/Users/${T_USERNAME}</string>
	</array>
	<key>name</key>
	<array>
		<string>${T_USERNAME}</string>
	</array>
	<key>passwd</key>
	<array>
		<string>********</string>
	</array>
	<key>realname</key>
	<array>
		<string>${T_REALNAME}</string>
	</array>
	<key>shell</key>
	<array>
		<string>${T_SHELL}</string>
	</array>
	<key>uid</key>
	<array>
		<string>${T_UID}</string>
	</array>
</dict>
</plist>
EOOF

#
# Create shadow hash file (620 bytes)
#
# 	1240 hexadecimal characters:
#	( 64 + 40 + 64 ) + ( 8 + 40 ) + ( 1024 )
#

mkdir -p ${MOUNT}/private/var/db/shadow/hash
# Init character pointer
i=0

# 168 hexadecimal characters
#
# 	64 characters for the LANMAN password (require "Windows sharing" enabled)
# 	40 characters for the "unsalted" SHA1 hash 
# 	64 characters for md5 hash, at least it's a power of 2 (2^6)
#
while (( i++ < 168 ))
do
   printf "0" >> ${MOUNT}/private/var/db/shadow/hash/${T_UUID}
done

# Hexadecimal salt (ie: FC2B0C2E)
T_HEX_SALT=`$OPENSSL rand 4 | $XXD -p -u`

# Binary salt (reverted from hex, plain hexdump style)
T_BIN_SALT=`printf ${T_HEX_SALT} | $XXD -r -u -p`

# Generated sha1 hash with provided password and binary salt
T_SHA1_HASH=`printf "%s%s" ${T_BIN_SALT} ${T_PASSWORD} | $OPENSSL dgst -sha1 | tr ' a-z' ' A-Z'`

# 8 characters for the hexadecimal salt
printf ${T_HEX_SALT} >> ${MOUNT}/private/var/db/shadow/hash/${T_UUID}
# 40 characters for the sha1 hash
printf ${T_SHA1_HASH} >> ${MOUNT}/private/var/db/shadow/hash/${T_UUID}

# iJump the hash!
i=$[$i+48]

# 1024 characters for "unknow reason (future big hash?)", at least it's another power of 2 (2^10)
while (( i++ < 1241 ))
do
   printf "0" >> ${MOUNT}/private/var/db/shadow/hash/${T_UUID}
done

#
# Create shadow hash file .state (not mandatory)
# 

T_CREATION_DATE=`date "+%Y-%m-%dT%H:%M:%SZ"`
sleep 1
T_LASTLOGIN_DATE=`date "+%Y-%m-%dT%H:%M:%SZ"`

cat >${MOUNT}/private/var/db/shadow/hash/${T_UUID}.state<<EOOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
   <dict>
	<key>CreationDate</key>
	<date>${T_CREATION_DATE}</date>
	<key>FailedLoginCount</key>
	<integer>0</integer>
	<key>LastLoginDate</key>
	<date>${T_LASTLOGIN_DATE}</date>
	<key>NewPasswordRequired</key>
	<integer>0</integer>
   </dict>
</plist>
EOOF

# Add to admin group
if [ "$ADMIN_USER" = "1" ];
then
	$GREP "<string>${T_USERNAME}</string>" "${MOUNT}/private/var/db/dslocal/nodes/Default/groups/admin.plist"
	if [ $? -eq 1 ];
	then
		cp ${MOUNT}/private/var/db/dslocal/nodes/Default/groups/admin.plist ${MOUNT}/private/var/db/dslocal/nodes/Default/groups/admin.plist.origin
		cat ${MOUNT}/private/var/db/dslocal/nodes/Default/groups/admin.plist.origin |
			sed "s/<string>root<\/string>/<string>root<\/string><string>${T_USERNAME}<\/string>/" > ${MOUNT}/private/var/db/dslocal/nodes/Default/groups/admin.plist
			# FIXME missing \n\t\t as ...>root</string>\n\t\t<string>${T_US...
		# Alternative
		#perl -pi -e "s|<string>root</string>|<string>root</string>\n\t\t<string>${T_USERNAME}</string>|g" ${MOUNT}/private/var/db/dslocal/nodes/Default/groups/admin.plist
	else
		echo "${T_USERNAME} is already found in admin group"
	fi	
fi

# Create and populate home directory 
mkdir -p ${MOUNT}${T_HOMEDIR}
chown ${T_UID}:${T_GID} ${MOUNT}${T_HOMEDIR}

# Auto reload DirectoryService in order to avoid reboot to make effect
if [ "$MOUNT" = "/" ]; then
	launchctl stop com.apple.DirectoryServices
fi

echo "User ${T_USERNAME} created"

# Symetry: restoring state 
/sbin/umount -f /dev
